package org.expath.pkg.saxon.java;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import net.sf.saxon.Configuration;
import net.sf.saxon.Controller;
import net.sf.saxon.Version;
import net.sf.saxon.trans.XPathException;
import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.expath.pkg.repo.PackageException;
import org.expath.pkg.repo.Repository;
import org.expath.pkg.repo.URISpace;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * ...
 *
 * @author Florent Georges
 * @date   2009-07-28
 */
public class ConfigHelper
{
    public ConfigHelper(Repository repo)
    {
        myRepo = repo;
        try {
            myState = (ConfigHelperState) STATE_CLASS.newInstance();
        }
        catch ( InstantiationException ex ) {
            throw new RuntimeException(ex);
        }
        catch ( IllegalAccessException ex ) {
            throw new RuntimeException(ex);
        }
    }

    public void config(Configuration config, URISpace space)
            throws PackageException
    {
        // CatalogManager mgr = new CatalogManager();
        // mgr.setCatalogFiles(getCatalogs(config));
        // TODO: for both resolvers, we should not set an absolute one, but
        // instead a resolver that wrap an existing one if any, to not loose
        // a user-defined resolver previously set...
        // config.setURIResolver(new CatalogResolver(mgr));
        // config.setModuleURIResolver(new QueryModuleResolver(mgr));
        Collection<String> catalogs = getCatalogs(config, space);
        CatalogManager mgr = myRepo.getCatalogManager(catalogs);
        if ( space == URISpace.XQUERY ) {
            config.setModuleURIResolver(new PkgModuleResolver(mgr));
        }
        else {
            config.setURIResolver(new CatalogResolver(mgr));
        }
        try {
            registerExtensionFunctions(config);
        }
        catch ( XPathException ex ) {
            throw new PackageException("Error configuring package repository", ex);
        }
    }

    public void config(Controller control, URISpace space)
            throws PackageException
    {
        if ( space == URISpace.XQUERY ) {
            throw new PackageException("Unsupported URI space on Controller: " + space);
        }
        else {
            control.setURIResolver(myRepo.getURIResolver(space));
        }
    }

    // ${repo}/.expath-pkg/xslt-catalog.xml
    // ${repo}/.expath-pkg/.saxon/xslt-wrappers.xml   -- if not 9.2 HE
    // ${repo}/.expath-pkg/.saxon/xslt-empty.xml      -- if 9.2 HE
    //     -or-
    // ${repo}/.expath-pkg/schematron-catalog.xml
    //     -or-
    // ${repo}/.expath-pkg/xquery-catalog.xml
    // ${repo}/.expath-pkg/.saxon/xquery-wrappers.xml   -- if not 9.2 HE
    // ${repo}/.expath-pkg/.saxon/xquery-empty.xml      -- if 9.2 HE
    //
    private Collection<String> getCatalogs(Configuration config, URISpace space)
            throws PackageException
    {
        Collection<String> cats = new ArrayList<String>();
        if ( space == URISpace.XSLT ) {
            cats.add(".expath-pkg/xslt-catalog.xml");
            cats.add(".expath-pkg/.saxon/" + myState.getStyleWrapperCatalog(config));
        }
        else if ( space == URISpace.SCHEMATRON ) {
            cats.add(".expath-pkg/schematron-catalog.xml");
        }
        else if ( space == URISpace.XQUERY ) {
            cats.add(".expath-pkg/xquery-catalog.xml");
            cats.add(".expath-pkg/.saxon/" + myState.getQueryWrapperCatalog(config));
        }
        else {
            throw new PackageException("Unsupported URI space: " + space);
        }
        // DEBUG: TODO: Make it dependent on an option (like the --verbose
        // level...)
        for ( String c : cats ) {
            System.err.println("CAT: " + c);
        }
        return cats;
    }

    private void registerExtensionFunctions(Configuration config)
            throws XPathException
    {
        myState.registerExtensionFunctions(config, this);
    }

    // TODO: The result could be cached for one single repo...
    // The class names are in ${repo}/*/saxon/jar/config.xml
    public Collection<String> getExtensionClasses(Configuration config)
            throws XPathException
    {
        ArrayList<String> res = new ArrayList<String>();
        File[] modules = new File(myRepo.getRoot(), ".expath-pkg/").listFiles();
        if ( modules == null ) {
            return res;
        }
        for ( File mod : modules ) {
            if ( mod.isDirectory() && ! mod.getName().equals(".saxon") ) {
                File conf_file = new File(mod, "saxon/classes.xml");
                if ( conf_file.exists() ) {
                    XMLReader parser = config.getSourceParser();
                    ConfigHandler handler = new ConfigHandler();
                    parser.setContentHandler(handler);
                    try {
                        InputStream in = new FileInputStream(conf_file);
                        parser.parse(new InputSource(in));
                    }
                    catch ( IOException ex ) {
                        throw new XPathException("Error parsing the EXPath config file for Saxon", ex);
                    }
                    catch ( SAXException ex ) {
                        throw new XPathException("Error parsing the EXPath config file for Saxon", ex);
                    }
                    res.addAll(handler.getFunctionClasses());
                }
            }
        }
        return res;
    }

    // TODO: See todos on getClassPath...
    public ClassLoader getClassLoader()
            throws XPathException
    {
        URL[] cp = getClassPath();
        return new URLClassLoader(cp);
    }

    // TODO: The result could be cached for one single repo...
    // The JAR file are looked for as ${repo}/saxon-lib/*.jar,
    // ${repo}/*/saxon/jar/*.jar and ${repo}/*/saxon/lib/*.jar.
    //
    // TODO: Actually, the resulting ClassLoader must be cached, as a same class
    // will be loaded twice if via 2 different loaders (even if they have the
    // exact same classpath... !!! (or maybe the resulting loaded class object?)
    private URL[] getClassPath()
            throws XPathException
    {
        ArrayList<URL> jars = new ArrayList<URL>();
        addJarsFromDir(jars, new File(myRepo.getRoot(), "saxon-lib/"));
        for ( File mod : myRepo.getRoot().listFiles() ) {
            if ( mod.isDirectory() ) {
                addJarsFromDir(jars, new File(mod, "saxon/jar/"));
                addJarsFromDir(jars, new File(mod, "saxon/lib/"));
            }
        }
        return jars.toArray(new URL[]{});
    }

    private void addJarsFromDir(List<URL> jars, File dir)
    {
        File[] children = dir.listFiles();
        if ( children == null ) {
            return;
        }
        for ( File f : children ) {
            if ( f.getName().endsWith(".jar") || f.getName().endsWith(".JAR") ) {
                try {
                    jars.add(f.toURI().toURL());
                }
                catch ( MalformedURLException ex ) {
                    // ignore and continue...
                }
            }
        }
    }

    private Repository myRepo;
    private ConfigHelperState myState;
    private static final Class STATE_CLASS;
    static {
        try {
            int[] ver = Version.getStructuredVersionNumber();
            STATE_CLASS = Class.forName("net.sf.saxon.expath.ConfigHelper" + ver[0] + ver[1]);
        }
        catch ( ClassNotFoundException ex ) {
            throw new RuntimeException(ex);
        }
    }

    public interface ConfigHelperState
    {
        void registerExtensionFunctions(Configuration config, ConfigHelper helper)
                throws XPathException;
        String getQueryWrapperCatalog(Configuration config);
        String getStyleWrapperCatalog(Configuration config);
    }

    private static class ConfigHandler
            extends DefaultHandler
    {
        @Override
        public void characters(char[] ch, int start, int length)
                throws SAXException
        {
            if ( myBuffer != null ) {
                myBuffer.append(ch, start, length);
            }
        }

        @Override
        public void endElement(String uri, String local, String qname)
                throws SAXException
        {
            if ( "".equals(uri) && "function".equals(local) ) {
                myFunctions.add(myBuffer.toString());
                myBuffer = null;
            }
        }

        @Override
        public void startElement(String uri, String local, String qname, Attributes attrs)
                throws SAXException
        {
            if ( "".equals(uri) && "function".equals(local) ) {
                myBuffer = new StringBuilder();
            }
        }

        public Collection<String> getFunctionClasses()
        {
            return myFunctions;
        }

        private StringBuilder myBuffer;
        private ArrayList<String> myFunctions = new ArrayList<String>();
    }
}
